/*
 * Copyright 2014, NICTA
 *
 * This software may be distributed and modified according to the terms of
 * the BSD 2-Clause license. Note that NO WARRANTY is provided.
 * See "LICENSE_BSD2.txt" for details.
 *
 * @TAG(NICTA_BSD)
 */

#include <sel4/sel4.h>
#include <elf/elf.h>
#include <string.h>
#include <assert.h>
#include <cspace/cspace.h>
#include <nfs/nfs.h>

#include "elf.h"
#include "frametable.h"
#include "pagetable.h"
#include "picoro.h"
#include "process.h"
#include "macros.h"

#include <vmem_layout.h>
#include <ut_manager/ut.h>
#include <mapping.h>

#include "debug.h"
#define verbose SOS_DEBUG_ELFLOAD
#include <sys/debug.h>
#include <sys/panic.h>

/* Bitmasks for Unix octal permissions */
#define USER_EXEC         (0b001000000)
#define GROUP_EXEC        (0b000001000)
#define OTHERS_EXEC       (0b000000001)
#define ANY_EXEC          (USER_EXEC | GROUP_EXEC | OTHERS_EXEC)
/* Macros for our very simplified interpretation of Unix permissions */
#define MODE_CAN_EXEC(x)  ((x) & (ANY_EXEC))

/* Arguments to be passed to the elf load coroutine */
typedef struct elf_load_args {
    sos_pcb *pcb;
    elf_cb cb;
    void *cookie;
    coro self;
} elf_load_args;

extern fhandle_t mnt_point;

/* Just wake up the waiting coroutine - assume there's only one */
static void _pagetable_done(int err, seL4_Word proc_vaddr, void *data) {
    coro task = (coro) data;
    dprintf(6, "Trying to wake up ELF coroutine\n");
    if (resumable(task)) {
        resume(task, (void *) err);
    } else {
        dprintf(6, "ELF coroutine not resumable...\n");
    }
}

/* callback when the filehandle for a given ELF is ready */
static void _elf_fh_ready(uintptr_t token, enum nfs_stat status,
                          fhandle_t *fh, fattr_t *fattr) {
    coro task = (coro) token;
    resume(task, (void *) status);
    resume(task, (void *) fh);
    resume(task, (void *) fattr);
    return;
}

/* Callback after a chunk has been read */
static void _elf_read_chunk(uintptr_t token, enum nfs_stat status,
                            fattr_t *fattr, int count, void *payload) {
    coro task = (coro) token;
    resume(task, (void *) status);
    resume(task, (void *) count);
    resume(task, (void *) payload);
    return;
}

/* Convert ELF permissions into seL4 permissions. */
static inline seL4_Word get_sel4_rights_from_elf(unsigned long permissions) {
    seL4_Word result = 0;

    if (permissions & PF_R)
        result |= seL4_CanRead;
    if (permissions & PF_X)
        result |= seL4_CanRead;
    if (permissions & PF_W)
        result |= seL4_CanWrite;

    return result;
}

/* Because this is inlined, it runs as part of the _elf_load coroutine */
static inline int _load_segment_into_vspace(sos_pcb *pcb, char *src,
                                     unsigned long segment_size,
                                     unsigned long file_size, unsigned long dst,
                                     unsigned long permissions,
                                     fhandle_t *fh, coro self) {
    if (!(file_size <= segment_size)) return 1;
    unsigned long pos = 0;
    unsigned long cur_dst = dst;
    while (pos < segment_size) {
        int err;
        int nbytes;
        size_t count;
        char *buf;

        page_attrs attrs = page_rwx;

        nbytes = PAGE_SIZE - (cur_dst & PAGE_MASK);
        if (pos == file_size) break;
        dprintf(7, "pos = %lu\tcur_dst = %lu\tsegment_size = %lu\tfile_size = %lu\n",
                pos, cur_dst, segment_size, file_size);

        /* Pin (implying a page fault) the desired page */
        dprintf(5, "elf_load_segment: trying to pin page\n");
        err = sos_page_pin(pcb, cur_dst, &_pagetable_done, (void *) self);
        err |= (int) yield(NULL);
        dprintf(5, "elf_load_segment: page_pin returned %d\n", err);
        if (err) return 1;

        /* Run a NFS read to get some of the segment */
        err = nfs_read(fh, (int) src,
                       MIN3(nbytes, file_size - pos, 1024),
                       &_elf_read_chunk, (uintptr_t) self);
        err |= (int) yield(NULL);
        count = (size_t) yield(NULL);
        buf = (char *) yield(NULL);
        if (err) return 1;

        /* Copy the desired amount into process memory */
        err = sos_copyin_nofault(pcb, cur_dst, buf, count, &nbytes);

        /* Not observable to I-cache yet, so flush the frame */
        err = sos_page_flush_icache(pcb, cur_dst);
        /* Really should not be able to fail */
        conditional_panic(err, "Pinned frame unflushable, somehow");

        /* Release the page, rendering it swappable again */
        err = sos_page_release(pcb, cur_dst);

        pos += nbytes;
        cur_dst += nbytes;
        src += nbytes;
    }
    return 0;
}

static void *_elf_load(void *_args) {

    int num_headers;
    int err;
    int i;
    elf_load_args *args = (elf_load_args *) _args;

    // Copy args into our own stack slice
    sos_pcb *pcb = args->pcb;
    elf_cb cb = args->cb;
    void *cookie = args->cookie;
    coro self = args->self;

    dprintf(6, "ELF load coroutine has started\n");

    fhandle_t fh, *ptr;
    fattr_t *fattr;
    size_t elf_size;
    size_t count;
    char *buf;
    char elf_header[256];
    seL4_Word pc;

    /* Set up NFS filehandle for the given ELF */
    err = nfs_lookup(&mnt_point, pcb->app_name, &_elf_fh_ready, (uintptr_t) self);
    err |= (int) yield(NULL);
    ptr = (fhandle_t *) yield(NULL);
    fattr = (fattr_t *) yield(NULL);
    if (err || !MODE_CAN_EXEC(fattr->mode)) {
        // Call back with error
        dprintf(6, "File was not executable / did not exist\n");
        (*cb)(1, 0, cookie);
        return NULL;
    }
    /* Copy the filehandle */
    memcpy(&fh, (void *) ptr, sizeof(struct fhandle));
    /* Store the filesize */
    elf_size = fattr->size;

    /* Fetch the ELF header from filesystem (first 0x32 / 50B) */
    /* Grab a little extra and hope we get the whole program header table */
    err = nfs_read(&fh, 0, 0x100, &_elf_read_chunk, (uintptr_t) self);
    err |= (int) yield(NULL);
    count = (size_t) yield(NULL);
    buf = (char *) yield(NULL);
    if (err || count < 0x32) {
        (*cb)(1, 0, cookie);
        return NULL;
    }

    /* Copy header into stack frame */
    memcpy(elf_header, buf, count);
    /* Grab ELF entry point */
    pc = elf_getEntryPoint(elf_header);

    /* Ensure that the ELF file looks sane. */
    if (elf_checkFile(elf_header)){
        (*cb)(SOS_PROCESS_INVAL, pc, cookie);
        return NULL;
    }

    /* Create process VM region to store ELF */
    err = sos_create_region(pcb, SOS_PROCESS_ELF_BOTTOM,
              PAGE_SHIFT((SOS_PROCESS_ELF_TOP - SOS_PROCESS_ELF_BOTTOM)),
              page_rwx);

    num_headers = elf_getNumProgramHeaders(elf_header);
    for (i = 0; i < num_headers; i++) {
        char *source_addr;
        unsigned long flags, file_size, segment_size, vaddr;

        /* Skip non-loadable segments (such as debugging data). */
        if (elf_getProgramHeaderType(elf_header, i) != PT_LOAD)
            continue;

        /* Fetch information about this segment. */
        source_addr = elf_getProgramHeaderOffset(elf_header, i);
        file_size = elf_getProgramHeaderFileSize(elf_header, i);
        segment_size = elf_getProgramHeaderMemorySize(elf_header, i);
        vaddr = elf_getProgramHeaderVaddr(elf_header, i);
        flags = elf_getProgramHeaderFlags(elf_header, i);

        /* Copy it across into the vspace. */
        dprintf(6, " * Loading segment %08x-->%08x\n", (int)vaddr, (int)(vaddr + segment_size));
        err = _load_segment_into_vspace(pcb, source_addr, segment_size,
                                  file_size, vaddr,
                                  get_sel4_rights_from_elf(flags) & seL4_AllRights,
                                  &fh, self);
        if (err) {
            (*cb)(err, pc, cookie);
            return NULL;
        }
    }

    dprintf(6, "elf_load: finished, calling back\n");
    (*cb)(0, pc, cookie);

    return NULL;
}

int elf_load(sos_pcb *pcb, elf_cb cb, void *cookie) {
    elf_load_args args = { pcb, cb, cookie, NULL };
    args.self = coroutine(&_elf_load);
    resume(args.self, (void *) &args);
    return 0;
}
